package com.jse.json;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Array;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;

import com.jse.Times;

public class JsonArray extends ArrayList<Object> {

    public JsonArray() {super();}

    public JsonArray(JSONTokener x) throws RuntimeException {
        this();
        if (x.nextClean() != '[') {
            throw new RuntimeException("A JSONArray text must start with '['"+x.toString());
        }
        
        char nextChar = x.nextClean();
        if (nextChar == 0) {
            // array is unclosed. No ']' found, instead EOF
            throw new RuntimeException("Expected a ',' or ']'"+x.toString());
        }
        if (nextChar != ']') {
            x.back();
            for (;;) {
                if (x.nextClean() == ',') {
                    x.back();
                    this.add((Object)null);
                } else {
                    x.back();
                    this.add(x.nextValue());
                }
                switch (x.nextClean()) {
                case 0:
                    // array is unclosed. No ']' found, instead EOF
                    throw new RuntimeException("Expected a ',' or ']'"+x.toString());
                case ',':
                    nextChar = x.nextClean();
                    if (nextChar == 0) {
                        // array is unclosed. No ']' found, instead EOF
                        throw new RuntimeException("Expected a ',' or ']'"+x.toString());
                    }
                    if (nextChar == ']') {
                        return;
                    }
                    x.back();
                    break;
                case ']':
                    return;
                default:
                    throw new RuntimeException("Expected a ',' or ']'"+x.toString());
                }
            }
        }
    }
    
    public JsonArray(String source) throws RuntimeException {
        this(new JSONTokener(source));
    }

    public JsonArray(Collection<?> collection) {addAll(collection,true);}
    public JsonArray(Map m) {addAll(m.values());}

    /**
     * Construct a JSONArray from an Iterable. This is a shallow copy.
     *
     * @param iter
     *            A Iterable collection.
     */
    public JsonArray(Iterable<?> iter) {
        this();
        if (iter == null) {
            return;
        }
        this.addAll(iter, true);
    }
    
    public JsonArray(ResultSet rs) {
		try {
			var md=rs.getMetaData();
			for(int i = 1; i <=md.getColumnCount(); i++) {
				if("DATETIME".equals(md.getColumnTypeName(i))) {
					add(Times.parse(rs.getTimestamp(i)));
				}else if("JSON".equals(md.getColumnTypeName(i))) {
					add(Json.parse(rs.getString(i)));
				}else {
					add(rs.getObject(i));
				}
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}

    public JsonArray(Object array) throws RuntimeException {
        this();
        if (array.getClass().isArray()) {
        	this.addAll(array, true);
        }else {
        	throw new RuntimeException(
                    "JSONArray initial value should be a string or collection or array.");
        }
    }

    /**
     * Construct a JSONArray with the specified initial capacity.
     *
     * @param initialCapacity
     *            the initial capacity of the JSONArray.
     * @throws JSONException
     *             If the initial capacity is negative.
     */
    public JsonArray(int initialCapacity) throws RuntimeException {
    	super(initialCapacity);
    }

    /**
     * Get the JSONArray associated with an index.
     *
     * @param index
     *            The index must be between 0 and length() - 1.
     * @return A JSONArray value.
     * @throws JSONException
     *             If there is no value for the index. or if the value is not a
     *             JSONArray
     */
    public JsonArray getJSONArray(int index) throws RuntimeException {
        Object object = this.get(index);
        if (object instanceof JsonArray) {
            return (JsonArray) object;
        }
        throw wrongValueFormatException(index, "JSONArray", object, null);
    }

    /**
     * Get the JSONObject associated with an index.
     *
     * @param index
     *            subscript
     * @return A JSONObject value.
     * @throws JSONException
     *             If there is no value for the index or if the value is not a
     *             JSONObject
     */
    public JsonObject getJSONObject(int index) throws RuntimeException {
        Object object = this.get(index);
        if (object instanceof JsonObject) {
            return (JsonObject) object;
        }
        throw wrongValueFormatException(index, "JSONObject", object, null);
    }

    /**
     * Get the number of elements in the JSONArray, included nulls.
     *
     * @return The length (or size).
     */
    public int length() {
        return this.size();
    }

    public Object opt(int index) {
        return (index < 0 || index >= this.length()) ? null : this.get(index);
    }

    public JsonArray optJSONArray(int index) {
        Object o = this.opt(index);
        return o instanceof JsonArray ? (JsonArray) o : null;
    }

    public JsonObject optJSONObject(int index) {
        Object o = this.opt(index);
        return o instanceof JsonObject ? (JsonObject) o : null;
    }

    @Override public boolean add(Object value){return super.add(value);}
    public JsonArray add(Collection value){super.add(Json.jsonArray(value));return this;}
    public JsonArray add(Map<?, ?> value) {super.add(Json.jsonObject(value));return this;}

    /**
     * Put a collection's elements in to the JSONArray.
     *
     * @param collection
     *            A Collection.
     * @return this. 
     */
    public JsonArray putAll(Collection<?> collection) {
        this.addAll(collection, false);
        return this;
    }
    
    /**
     * Put an Iterable's elements in to the JSONArray.
     *
     * @param iter
     *            An Iterable.
     * @return this. 
     */
    public JsonArray putAll(Iterable<?> iter) {
        this.addAll(iter, false);
        return this;
    }

    /**
     * Put a JSONArray's elements in to the JSONArray.
     *
     * @param array
     *            A JSONArray.
     * @return this. 
     */
    public JsonArray putAll(JsonArray array) {
        addAll(array);
        return this;
    }

    /**
     * Put an array's elements in to the JSONArray.
     *
     * @param array
     *            Array. If the parameter passed is null, or not an array or Iterable, an
     *            exception will be thrown.
     * @return this. 
     *
     * @throws JSONException
     *            If not an array, JSONArray, Iterable or if an value is non-finite number.
     * @throws NullPointerException
     *            Thrown if the array parameter is null.
     */
    public JsonArray putAll(Object array) throws RuntimeException {
        this.addAll(array, false);
        return this;
    }
    
    /**
     * Creates a JSONPointer using an initialization string and tries to 
     * match it to an item within this JSONArray. For example, given a
     * JSONArray initialized with this document:
     * <pre>
     * [
     *     {"b":"c"}
     * ]
     * </pre>
     * and this JSONPointer string: 
     * <pre>
     * "/0/b"
     * </pre>
     * Then this method will return the String "c"
     * A JSONPointerException may be thrown from code called by this method.
     *
     * @param jsonPointer string that can be used to create a JSONPointer
     * @return the item matched by the JSONPointer, otherwise null
     */
    public Object query(String jsonPointer) {
        return query(new JSONPointer(jsonPointer));
    }
    
    /**
     * Uses a user initialized JSONPointer  and tries to 
     * match it to an item within this JSONArray. For example, given a
     * JSONArray initialized with this document:
     * <pre>
     * [
     *     {"b":"c"}
     * ]
     * </pre>
     * and this JSONPointer: 
     * <pre>
     * "/0/b"
     * </pre>
     * Then this method will return the String "c"
     * A JSONPointerException may be thrown from code called by this method.
     *
     * @param jsonPointer string that can be used to create a JSONPointer
     * @return the item matched by the JSONPointer, otherwise null
     */
    public Object query(JSONPointer jsonPointer) {
        return jsonPointer.queryFrom(this);
    }
    
    /**
     * Queries and returns a value from this object using {@code jsonPointer}, or
     * returns null if the query fails due to a missing key.
     * 
     * @param jsonPointer the string representation of the JSON pointer
     * @return the queried value or {@code null}
     * @throws IllegalArgumentException if {@code jsonPointer} has invalid syntax
     */
    public Object optQuery(String jsonPointer) {
    	return optQuery(new JSONPointer(jsonPointer));
    }
    
    /**
     * Queries and returns a value from this object using {@code jsonPointer}, or
     * returns null if the query fails due to a missing key.
     * 
     * @param jsonPointer The JSON pointer
     * @return the queried value or {@code null}
     * @throws IllegalArgumentException if {@code jsonPointer} has invalid syntax
     */
    public Object optQuery(JSONPointer jsonPointer) {
    	return jsonPointer.queryFrom(this);
    }

    /**
     * Determine if two JSONArrays are similar.
     * They must contain similar sequences.
     *
     * @param other The other JSONArray
     * @return true if they are equal
     */
    public boolean similar(Object other) {
        if (!(other instanceof JsonArray)) {
            return false;
        }
        int len = this.length();
        if (len != ((JsonArray)other).length()) {
            return false;
        }
        for (int i = 0; i < len; i += 1) {
            Object valueThis = this.get(i);
            Object valueOther = ((JsonArray)other).get(i);
            if(valueThis == valueOther) {
            	continue;
            }
            if(valueThis == null) {
            	return false;
            }
            if (valueThis instanceof JsonObject) {
                if (!((JsonObject)valueThis).similar(valueOther)) {
                    return false;
                }
            } else if (valueThis instanceof JsonArray) {
                if (!((JsonArray)valueThis).similar(valueOther)) {
                    return false;
                }
            } else if (valueThis instanceof Number n1 && valueOther instanceof Number n2) {
                if (n1.doubleValue()!=n2.doubleValue()) {
                	return false;
                }
            } else if (!valueThis.equals(valueOther)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Produce a JSONObject by combining a JSONArray of names with the values of
     * this JSONArray.
     *
     * @param names
     *            A JSONArray containing a list of key strings. These will be
     *            paired with the values.
     * @return A JSONObject, or null if there are no names or if this JSONArray
     *         has no values.
     * @throws JSONException
     *             If any of the names are null.
     */
    public JsonObject toJSONObject(JsonArray names) throws RuntimeException {
        if (names == null || names.isEmpty() || this.isEmpty()) {
            return null;
        }
        JsonObject jo = new JsonObject(names.length());
        for (int i = 0; i < names.length(); i += 1) {
            jo.put(names.get(i).toString(), this.opt(i));
        }
        return jo;
    }

    /**
     * Make a JSON text of this JSONArray. For compactness, no unnecessary
     * whitespace is added. If it is not possible to produce a syntactically
     * correct JSON text then null will be returned instead. This could occur if
     * the array contains an invalid number.
     * <p><b>
     * Warning: This method assumes that the data structure is acyclical.
     * </b>
     *
     * @return a printable, displayable, transmittable representation of the
     *         array.
     */
    @Override
    public String toString() {
        try {
            return this.toString(0);
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * Make a pretty-printed JSON text of this JSONArray.
     * 
     * <p>If <pre> {@code indentFactor > 0}</pre> and the {@link JsonArray} has only
     * one element, then the array will be output on a single line:
     * <pre>{@code [1]}</pre>
     * 
     * <p>If an array has 2 or more elements, then it will be output across
     * multiple lines: <pre>{@code
     * [
     * 1,
     * "value 2",
     * 3
     * ]
     * }</pre>
     * <p><b>
     * Warning: This method assumes that the data structure is acyclical.
     * </b>
     * 
     * @param indentFactor
     *            The number of spaces to add to each level of indentation.
     * @return a printable, displayable, transmittable representation of the
     *         object, beginning with <code>[</code>&nbsp;<small>(left
     *         bracket)</small> and ending with <code>]</code>
     *         &nbsp;<small>(right bracket)</small>.
     * @throws JSONException if a called function fails
     */
    @SuppressWarnings("resource")
    public String toString(int indentFactor) throws RuntimeException {
        StringWriter sw = new StringWriter();
        synchronized (sw.getBuffer()) {
            return this.write(sw, indentFactor, 0).toString();
        }
    }

    /**
     * Write the contents of the JSONArray as JSON text to a writer. For
     * compactness, no whitespace is added.
     * <p><b>
     * Warning: This method assumes that the data structure is acyclical.
     *</b>
     * @param writer the writer object
     * @return The writer.
     * @throws JSONException if a called function fails
     */
    public Writer write(Writer writer) throws RuntimeException {
        return this.write(writer, 0, 0);
    }

    /**
     * Write the contents of the JSONArray as JSON text to a writer.
     * 
     * <p>If <pre>{@code indentFactor > 0}</pre> and the {@link JsonArray} has only
     * one element, then the array will be output on a single line:
     * <pre>{@code [1]}</pre>
     * 
     * <p>If an array has 2 or more elements, then it will be output across
     * multiple lines: <pre>{@code
     * [
     * 1,
     * "value 2",
     * 3
     * ]
     * }</pre>
     * <p><b>
     * Warning: This method assumes that the data structure is acyclical.
     * </b>
     *
     * @param writer
     *            Writes the serialized JSON
     * @param indentFactor
     *            The number of spaces to add to each level of indentation.
     * @param indent
     *            The indentation of the top level.
     * @return The writer.
     * @throws JSONException if a called function fails or unable to write
     */
    @SuppressWarnings("resource")
    public Writer write(Writer writer, int indentFactor, int indent)
            throws RuntimeException {
        try {
            boolean needsComma = false;
            int length = this.length();
            writer.write('[');

            if (length == 1) {
                try {
                    JsonObject.writeValue(writer, this.get(0),
                            indentFactor, indent,false);
                } catch (Exception e) {
                    throw new RuntimeException("Unable to write JSONArray value at index: 0", e);
                }
            } else if (length != 0) {
                final int newIndent = indent + indentFactor;

                for (int i = 0; i < length; i += 1) {
                    if (needsComma) {
                        writer.write(',');
                    }
                    if (indentFactor > 0) {
                        writer.write('\n');
                    }
                    JsonObject.indent(writer, newIndent);
                    try {
                        JsonObject.writeValue(writer, this.get(i),
                                indentFactor, newIndent,false);
                    } catch (Exception e) {
                        throw new RuntimeException("Unable to write JSONArray value at index: " + i, e);
                    }
                    needsComma = true;
                }
                if (indentFactor > 0) {
                    writer.write('\n');
                }
                JsonObject.indent(writer, indent);
            }
            writer.write(']');
            return writer;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Add a collection's elements to the JSONArray.
     *
     * @param collection
     *            A Collection.
     * @param wrap
     *            {@code true} to call {@link JsonObject#wrap(Object)} for each item,
     *            {@code false} to add the items directly
     *            
     */
    private void addAll(Collection<?> collection, boolean wrap) {
        this.ensureCapacity(this.size() + collection.size());
        if (wrap) {
            for (Object o: collection){
            	if(o!=null&&o.getClass().getName().endsWith(".ScriptObjectMirror")) {
            		String s=o.toString();
            		if(s.startsWith("function(")) {
        			}else if(("[object Array]").equals(s)){
        				add(new JsonArray(((Map)o).values()));
        			}else {
        				add(JsonObject.wrap(o));
        			}
        		}else {
        			add(JsonObject.wrap(o));
        		}
            }
        } else {
            for (Object o: collection){
                add(o);
            }
        }
    }

    /**
     * Add an Iterable's elements to the JSONArray.
     *
     * @param iter
     *            An Iterable.
     * @param wrap
     *            {@code true} to call {@link JsonObject#wrap(Object)} for each item,
     *            {@code false} to add the items directly
     */
    private void addAll(Iterable<?> iter, boolean wrap) {
        if (wrap) {
            for (Object o: iter){
                this.add(JsonObject.wrap(o));
            }
        } else {
            for (Object o: iter){
                this.add(o);
            }
        }
    }
    
    /**
     * Add an array's elements to the JSONArray.
     *
     * @param array
     *            Array. If the parameter passed is null, or not an array,
     *            JSONArray, Collection, or Iterable, an exception will be
     *            thrown.
     * @param wrap
     *            {@code true} to call {@link JsonObject#wrap(Object)} for each item,
     *            {@code false} to add the items directly
     *
     * @throws JSONException
     *            If not an array or if an array value is non-finite number.
     * @throws NullPointerException
     *            Thrown if the array parameter is null.
     */
    private void addAll(Object array, boolean wrap) throws RuntimeException {
        if (array.getClass().isArray()) {
            int length = Array.getLength(array);
            this.ensureCapacity(this.size() + length);
            if (wrap) {
                for (int i = 0; i < length; i += 1) {
                    add(JsonObject.wrap(Array.get(array, i)));
                }
            } else {
                for (int i = 0; i < length; i += 1) {
                    add(Array.get(array, i));
                }
            }
        } else if (array instanceof JsonArray) {
            // use the built in array list `addAll` as all object
            // wrapping should have been completed in the original
            // JSONArray
            this.addAll(((JsonArray)array));
        } else if (array instanceof Collection) {
            this.addAll((Collection<?>)array, wrap);
        } else if (array instanceof Iterable) {
            this.addAll((Iterable<?>)array, wrap);
        } else {
            throw new RuntimeException(
                    "JSONArray initial value should be a string or collection or array.");
        }
    }
    
    /**
     * Create a new JSONException in a common format for incorrect conversions.
     * @param idx index of the item
     * @param valueType the type of value being coerced to
     * @param cause optional cause of the coercion failure
     * @return JSONException that can be thrown.
     */
    private static RuntimeException wrongValueFormatException(
            int idx,
            String valueType,
            Object value,
            Throwable cause) {
        if(value == null) {
            return new RuntimeException(
                    "JSONArray[" + idx + "] is not a " + valueType + " (null)."
                    , cause);
        }
        // don't try to toString collections or known object types that could be large.
        if(value instanceof Map || value instanceof Iterable || value instanceof JsonObject) {
            return new RuntimeException(
                    "JSONArray[" + idx + "] is not a " + valueType + " (" + value.getClass() + ")."
                    , cause);
        }
        return new RuntimeException(
                "JSONArray[" + idx + "] is not a " + valueType + " (" + value.getClass() + " : " + value + ")."
                , cause);
    }

	public JsonArray set(Object v) {add(v);return this;}

	public String toXml() {return XML.toString(this);}

}
